/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.meta.service.impl;


import static org.unidata.mdm.meta.service.impl.MetaModelImportServiceImpl.META_MODEL_PATH;

import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

import javax.xml.bind.JAXBException;
import javax.xml.namespace.QName;

import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.unidata.mdm.core.context.UpsertLargeObjectContext;
import org.unidata.mdm.core.context.UpsertUserEventRequestContext;
import org.unidata.mdm.core.dto.UserEventDTO;
import org.unidata.mdm.core.service.LargeObjectsService;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.service.UserService;
import org.unidata.mdm.core.type.lob.LargeObjectAcceptance;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.constant.IEConstants;
import org.unidata.mdm.meta.context.ExportContext;
import org.unidata.mdm.meta.exception.MetaExceptionIds;
import org.unidata.mdm.meta.service.MetaModelExportService;
import org.unidata.mdm.meta.service.SecurityIOService;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.meta.type.model.measurement.MeasurementUnitsModel;
import org.unidata.mdm.meta.util.ZipUtils;
import org.unidata.mdm.system.exception.PlatformFailureException;
import org.unidata.mdm.system.serialization.xml.XmlObjectSerializer;
import org.unidata.mdm.system.util.TextUtils;


/**
 * @author maria.chistyakova
 * @since 17.04.2020
 */
@Service
public class MetaModelExportServiceImp implements MetaModelExportService {

    private static final Logger LOGGER = LoggerFactory.getLogger(MetaModelExportServiceImp.class);

    // TODO: 17.04.2020 move to constant file or remove
    private static final String XML_EXTENSION = ".xml";

    private static final String MODEL = "model";

    private static final String MEASURE = "measure";


    /**
     * The Constant MEASUREMENT_VALUE_QNAME.
     */
    // TODO: 15.05.2020 MOVE TO CONVERTER
    private static final QName MEASUREMENT_VALUE_QNAME = new QName("http://meta.mdm.unidata.com/", "MeasurementValues",
            "measurementValues");


    @Autowired
    private UserService userService;

    @Autowired
    private MetaModelService metaModelService;

    @Autowired
    private LargeObjectsService largeObjectsService;

    @Autowired
    private SecurityIOService securityIOService;

    @Override
    public void exportModel(String storageId, Path rootPath, ExportContext exportContext) {
        try {

            rootPath = Paths.get(rootPath.toString(), META_MODEL_PATH);

            exportModel(storageId, rootPath);
            exportMeasure(storageId, rootPath);
            securityIOService.exportSecurityObjects(rootPath, exportContext);

        } catch (IOException e) {
            throw new PlatformFailureException(
                    "Unable to create zip file for metamodel. Exception occured.",
                    MetaExceptionIds.EX_META_CANNOT_ASSEMBLE_MODEL
            );
        }
    }

    @Override
    public void finishExport(Path rootPath) {
        Path zipModel;
        try {
            zipModel = ZipUtils.zipDir(rootPath);

            try (InputStream is = new FileInputStream(zipModel.toFile())) {
                largeObjectProcessing(zipModel, is);
            }

        } catch (IOException e) {
            throw new PlatformFailureException(
                    "Unable to create zip file for metamodel. Exception occured.",
                    MetaExceptionIds.EX_META_CANNOT_ASSEMBLE_MODEL
            );
        } finally {
            try {
                Files.walkFileTree(rootPath, getVisitor());

                Files.deleteIfExists(Paths.get(rootPath.toString() + ".zip"));
            } catch (IOException e) {
                LOGGER.error("Unable to delete temporary metamodel export files", e);
            }
        }

    }

    private String exportFileName(String storageId) {
        return storageId + getCurrentDateInExportFormat();
    }

    private String getCurrentDateInExportFormat() {
        return DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd_HH-mm-ss");
    }

    private UpsertUserEventRequestContext getUpsertUserEventRequestContext() {
        return UpsertUserEventRequestContext.builder()
                .login(SecurityUtils.getCurrentUserName())
                .type("META_FULL_EXPORT")
                .content(TextUtils.getText(IEConstants.DATA_EXPORT_METADATA_SUCCESS)).build();
    }

    private void largeObjectProcessing(Path zipModel, InputStream is) {

        UpsertUserEventRequestContext uueCtx = getUpsertUserEventRequestContext();
        UserEventDTO userEventDTO = userService.upsert(uueCtx);
        // save result and attach it to the early created user event

        UpsertLargeObjectContext slorCtx = UpsertLargeObjectContext.builder()
                .subjectId(userEventDTO.getId())
                .mimeType("application/zip")
                .binary(true)
                .input(is)
                .filename(zipModel.getFileName().toString())
                .acceptance(LargeObjectAcceptance.ACCEPTED)
                .build();

        largeObjectsService.saveLargeObject(slorCtx);
    }

    private SimpleFileVisitor<Path> getVisitor() {
        return new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.deleteIfExists(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.deleteIfExists(dir);
                return FileVisitResult.CONTINUE;
            }
        };
    }

    private void exportMeasure(String storageId, Path rootPath) throws IOException {
        MeasurementUnitsModel measurementValueDef = assembleMeasurement(storageId);
        Path measurementFolder = Files.createDirectories(Paths.get(rootPath.toString(), MEASURE));
        String result = convertMeasure(measurementValueDef);
        Path file = Files.createFile(Paths.get(measurementFolder.toString(), MEASURE + XML_EXTENSION));
        write(result, file);
    }

    private void exportModel(String storageId, Path rootPath) throws IOException {
        DataModel model = metaModelService.instance(Descriptors.DATA, storageId, null).toSource();
        Path modelFolder = Files.createDirectories(Paths.get(rootPath.toString(), MODEL));
        String result = XmlObjectSerializer.getInstance().toXmlString(model, false);
        Path file = Files.createFile(Paths.get(modelFolder.toString(), MODEL + XML_EXTENSION));
        write(result, file);
    }

    private void write(String string, Path file) throws IOException {
        try (BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(file.toString()), StandardCharsets.UTF_8.name()))) {
            bw.write(string);
        }
    }

    private MeasurementUnitsModel assembleMeasurement(String storageId) {
        return metaModelService.instance(Descriptors.MEASUREMENT_UNITS, storageId, null).toSource();
    }

    /**
     * Convert measure.
     *
     * @param valueDef the value def
     * @return the string
     * @throws JAXBException the JAXB exception
     */
    public static String convertMeasure(MeasurementUnitsModel valueDef) {
        return XmlObjectSerializer.getInstance().toXmlString(valueDef, false);
    }

}
